<template>
    <transition name="viewer-fade" appear>
        <div ref="wrapperRef" :tabindex="-1" :class="ns.e('wrapper')" :style="{ zIndex }" v-if="showPreview"
            @touchstart.stop="touchStart" class="untouchable">
            <div :class="ns.e('mask')" @click.self="closeOnModal && !mobile && hide()" ref="maskRef" />
            <!-- CLOSE -->
            <span :class="[ns.e('btn'), ns.e('close')]" @click="hide">
                <el-icon>
                    <Close />
                </el-icon>
            </span>
            <!-- ARROW -->
            <template v-if="!isSingleImage">
                <span :class="arrowPrevKls" @click="prev">
                    <el-icon>
                        <ArrowLeft />
                    </el-icon>
                </span>
                <span :class="arrowNextKls" @click="next">
                    <el-icon>
                        <ArrowRight />
                    </el-icon>
                </span>
            </template>
            <!-- ACTIONS -->
            <div :class="[ns.e('btn'), ns.e('actions')]">
                <div :class="ns.e('actions__inner')">
                    <el-icon @click="handleActions('zoomOut')" v-if="!mobile">
                        <ZoomOut />
                    </el-icon>
                    <el-icon @click="handleActions('zoomIn')" v-if="!mobile">
                        <ZoomIn />
                    </el-icon>
                    <el-icon @click="handleActions('anticlockwise')" v-if="mobile">
                        <RefreshLeft />
                    </el-icon>
                    <i :class="ns.e('actions__divider')" />
                    <el-icon @click="toggleMode">
                        <component :is="mode.icon" />
                    </el-icon>
                    <i :class="ns.e('actions__divider')" />
                    <el-icon @click="handleActions('anticlockwise')" v-if="!mobile">
                        <RefreshLeft />
                    </el-icon>
                    <el-icon @click="handleActions('clockwise')">
                        <RefreshRight />
                    </el-icon>
                </div>
            </div>
            <!-- CANVAS -->
            <div :class="ns.e('canvas')">
                <transition name="animate__animated " enter-active-class="animate__fadeInLeft animate__fast"
                    leave-active-class="animate__fadeOutRightBig animate__faster" mode="out-in">
                    <img ref="imgRef" :key="currentImg" :src="currentImg" :style="imgStyle" :class="ns.e('img')"
                        :crossorigin="crossorigin" @load="handleImgLoad" @error="handleImgError"
                        @mousedown="handleMouseDown" />
                </transition>
            </div>
            <slot />
        </div>
    </transition>
</template>
<script setup>
import { useEventListener } from '@vueuse/core'
import { throttle } from 'lodash-unified'
import { useNamespace, useZIndex } from 'element-plus/dist/index.full.js'
import { ArrowLeft, ArrowRight, Close, FullScreen, RefreshLeft, RefreshRight, ScaleToOriginal, ZoomIn, ZoomOut } from '@element-plus/icons-vue'

const mobile = /(iPhone|Mobile|Android)/.test(navigator.userAgent)

const EVENT_CODE = {
    tab: "Tab",
    enter: "Enter",
    space: "Space",
    left: "ArrowLeft",
    up: "ArrowUp",
    right: "ArrowRight",
    down: "ArrowDown",
    esc: "Escape",
    delete: "Delete",
    backspace: "Backspace",
    numpadEnter: "NumpadEnter",
    pageUp: "PageUp",
    pageDown: "PageDown",
    home: "Home",
    end: "End"
}

const keysOf = (arr) => { return Object.keys(arr) }
const showPreview = ref(false)
const modes = {
    CONTAIN: {
        name: 'contain',
        icon: markRaw(FullScreen),
    },
    ORIGINAL: {
        name: 'original',
        icon: markRaw(ScaleToOriginal),
    },
}

defineOptions({
    name: 'ElImageViewer',
})

const props = defineProps({
    /**
     * @description preview link list.
     */
    url: {
        type: [String, Array],
        required: true
    },
    /**
     * @description preview backdrop z-index.
     */
    zIndex: {
        type: Number,
    },
    /**
     * @description the initial preview image index, less than or equal to the length of `url-list`.
     */
    index: {
        type: [String, Number],
        default: 0,
    },
    /**
     * @description whether preview is infinite.
     */
    infinite: {
        type: Boolean,
        default: true,
    },
    /**
     * @description whether user can emit close event when clicking backdrop.
     */
    closeOnModal: {
        type: Boolean,
        default: false
    },
    /**
     * @description whether the image-viewer can be closed by pressing ESC.
     */
    closeOnPressEscape: {
        type: Boolean,
        default: true,
    },
    /**
     * @description the zoom rate of the image viewer zoom event.
     */
    zoomRate: {
        type: Number,
        default: 1.2,
    },
    /**
     * @description the min scale of the image viewer zoom event.
     */
    minScale: {
        type: Number,
        default: 0.2,
    },
    /**
     * @description the max scale of the image viewer zoom event.
     */
    maxScale: {
        type: Number,
        default: 7,
    },
    /**
     * @description set HTML attribute: crossorigin-> anonymous  use-credentials  null
     */
    crossorigin: {
        type: String,
    },
})
const emit = defineEmits(["close", "switch", "rotate"])

const urlList = computed(() => {
    if (typeof props.url === 'string') {
        return [props.url]
    } else if (props.url instanceof Array) {
        return props.url
    } else {
        return []
    }
})

const ns = useNamespace('image-viewer')
const { nextZIndex } = useZIndex(9999)
const wrapperRef = ref()
const imgRef = ref(null)

const scopeEventListener = effectScope()

const loading = ref(true)
const activeIndex = ref(parseInt(props.index) >= 0 ? parseInt(props.index) : 0)
const mode = shallowRef(modes.CONTAIN)
const transform = ref({
    scale: 1,
    deg: 0,
    offsetX: 0,
    offsetY: 0,
    enableTransition: false,
})
const zIndex = ref(props.zIndex ?? nextZIndex())

const isSingleImage = computed(() => {
    return urlList.value.length <= 1
})

const isFirst = computed(() => {
    return activeIndex.value === 0
})

const isLast = computed(() => {
    return activeIndex.value === urlList.value.length - 1
})

const currentImg = computed(() => {
    return urlList.value[activeIndex.value]
})

const arrowPrevKls = computed(() => [
    ns.e('btn'),
    ns.e('prev'),
    ns.is('disabled', !props.infinite && isFirst.value),
])

const arrowNextKls = computed(() => [
    ns.e('btn'),
    ns.e('next'),
    ns.is('disabled', !props.infinite && isLast.value),
])

const imgStyle = computed(() => {
    const { scale, deg, offsetX, offsetY, enableTransition } = transform.value
    let translateX = offsetX / scale
    let translateY = offsetY / scale

    switch (deg % 360) {
        case 90:
        case -270:
            ;[translateX, translateY] = [translateY, -translateX]
            break
        case 180:
        case -180:
            ;[translateX, translateY] = [-translateX, -translateY]
            break
        case 270:
        case -90:
            ;[translateX, translateY] = [-translateY, translateX]
            break
    }

    const style = {
        transform: `scale(${scale}) rotate(${deg}deg) translate(${translateX}px, ${translateY}px)`,
        transition: enableTransition ? 'transform .3s' : '',
    }
    if (mode.value.name === modes.CONTAIN.name) {
        style.maxWidth = style.maxHeight = '100%'
    }
    return style
})

const maskRef = ref(null)
function hide() {
    unregisterEventListener()
    emit('close')
    maskRef.value.style.pointerEvents = "none"
    showPreview.value = false
}

function registerEventListener() {
    const keydownHandler = throttle((e) => {
        switch (e.code) {
            // ESC
            case EVENT_CODE.esc:
                props.closeOnPressEscape && hide()
                break
            // SPACE
            case EVENT_CODE.space:
                toggleMode()
                break
            // LEFT_ARROW
            case EVENT_CODE.left:
                prev()
                break
            // UP_ARROW
            case EVENT_CODE.up:
                handleActions('zoomIn')
                break
            // RIGHT_ARROW
            case EVENT_CODE.right:
                next()
                break
            // DOWN_ARROW
            case EVENT_CODE.down:
                handleActions('zoomOut')
                break
        }
    })
    const mousewheelHandler = throttle((e) => {
        const delta = e.deltaY || e.deltaX
        handleActions(delta < 0 ? 'zoomIn' : 'zoomOut', {
            zoomRate: props.zoomRate,
            enableTransition: false,
        })
    })

    scopeEventListener.run(() => {
        useEventListener(document, 'keydown', keydownHandler)
        useEventListener(document, 'wheel', mousewheelHandler)
    })
}

scopeEventListener.run(() => { useEventListener(document.querySelector(".tabbar-box"), "touchstart", hide) })

function unregisterEventListener() {
    scopeEventListener.stop()
}

function handleImgLoad() {
    loading.value = false
}

function handleImgError(e) {
    loading.value = false
        ; (e.target).alt = "图片加载失败"
}

function handleMouseDown(e) {
    if (loading.value || e.button !== 0 || !wrapperRef.value) return
    transform.value.enableTransition = false

    const { offsetX, offsetY } = transform.value
    const startX = e.pageX
    const startY = e.pageY

    const dragHandler = throttle((ev) => {
        transform.value = {
            ...transform.value,
            offsetX: offsetX + ev.pageX - startX,
            offsetY: offsetY + ev.pageY - startY,
        }
    })
    const removeMousemove = useEventListener(document, 'mousemove', dragHandler)
    const removeMouseup = useEventListener(document, 'mouseup', () => {
        removeMousemove()
        removeMouseup()
    })

    e.preventDefault()
}

function reset() {
    transform.value = {
        scale: 1,
        deg: 0,
        offsetX: 0,
        offsetY: 0,
        enableTransition: false,
    }
}

function toggleMode() {
    if (loading.value) return

    const modeNames = keysOf(modes)
    const modeValues = Object.values(modes)
    const currentMode = mode.value.name
    const index = modeValues.findIndex((i) => i.name === currentMode)
    const nextIndex = (index + 1) % modeNames.length
    mode.value = modes[modeNames[nextIndex]]
    reset()
}

function setActiveItem(index) {
    const len = urlList.value.length
    activeIndex.value = (index + len) % len
}

function prev() {
    if (isFirst.value && !props.infinite) return
    setActiveItem(activeIndex.value - 1)
}

function next() {
    if (isLast.value && !props.infinite) return
    setActiveItem(activeIndex.value + 1)
}

function handleActions(action, options = {}) {
    if (loading.value) return
    const { minScale, maxScale } = props
    const { zoomRate, rotateDeg, enableTransition } = {
        zoomRate: props.zoomRate,
        rotateDeg: 90,
        enableTransition: true,
        ...options,
    }
    switch (action) {
        case 'zoomOut':
            if (transform.value.scale > minScale) {
                transform.value.scale = Number.parseFloat(
                    (transform.value.scale / zoomRate).toFixed(3)
                )
            }
            break
        case 'zoomIn':
            if (transform.value.scale < maxScale) {
                transform.value.scale = Number.parseFloat(
                    (transform.value.scale * zoomRate).toFixed(3)
                )
            }
            break
        case 'clockwise':
            transform.value.deg += rotateDeg
            emit('rotate', transform.value.deg)
            break
        case 'anticlockwise':
            transform.value.deg -= rotateDeg
            emit('rotate', transform.value.deg)
            break
    }
    transform.value.enableTransition = enableTransition
}

watch(currentImg, () => {
    nextTick(() => {
        if (!imgRef?.complete) {
            loading.value = true
        }
    })
})

watch(activeIndex, (val) => {
    reset()
    emit('switch', val)
})

onMounted(() => {
    registerEventListener()
    // add tabindex then wrapperRef can be focusable via Javascript
    // focus wrapperRef so arrow key can't cause inner scroll behavior underneath
    wrapperRef.value?.focus?.()
    showPreview.value = true
})

const touchStart = (e) => {
    if (e.touches.length === 2) {
        scaleImage(e)
    } else if (e.touches.length === 1) {
        moveImage(e)
    }
}

// 双指触摸监听器功能实现
const scaleImage = (e) => {
    let firstTouchPoint = e.touches[0]
    let secondTouchPoint = e.touches[1]

    // 第一个触摸点的坐标
    const pageX = firstTouchPoint.pageX
    const pageY = firstTouchPoint.pageY
    let moveable = true
    let pageX2 = secondTouchPoint.pageX
    let pageY2 = secondTouchPoint.pageY

    let originScale = transform.value.scale || 1

    // 获取坐标之间的距离
    const getDistance = (start, stop) => {
        return Math.hypot(stop.x - start.x, stop.y - start.y)
    }

    const wrapperTouchmove = (ev) => {
        if (!moveable) {
            console.log("unmoveable")
            return;
        }
        firstTouchPoint = ev.touches[0]
        secondTouchPoint = ev.touches[1]

        // 双指缩放比例计算
        let zoom = getDistance({ x: firstTouchPoint.pageX, y: firstTouchPoint.pageY }, { x: secondTouchPoint.pageX, y: secondTouchPoint.pageY }) /
            getDistance({ x: pageX, y: pageY }, { x: pageX2, y: pageY2 })
        // 应用在元素上的缩放比例
        let newScale = originScale * zoom
        // 最大缩放比例限制
        if (newScale >= props.maxScale || newScale <= props.minScale) {
            console.log("超出范围")
            return
        }
        // 记住使用的缩放值
        transform.value.scale = newScale
    }

    const removeMousemove = useEventListener(document, 'touchmove', throttle(wrapperTouchmove))

    const removeTouchcancel = useEventListener(document, 'touchcancel', () => {
        moveable = false
        removeMousemove()
        removeTouchend()
        removeTouchcancel()
    })

    const removeTouchend = useEventListener(document, 'touchend', () => {
        moveable = false
        removeMousemove()
        removeTouchend()
        removeTouchcancel()
    })
}

// 单指触摸监听器功能实现
const moveImage = (e) => {
    // 单指变量
    let initialX = 0 // 初始点击X点位
    let initialY = 0 // 初始点击Y点位
    transform.value.enableTransition = false
    // 拿到当前的位置
    initialX = e.touches[0].clientX - transform.value.offsetX;
    initialY = e.touches[0].clientY - transform.value.offsetY;

    const removeMousemove = useEventListener(document, 'touchmove', throttle((ev) => {
        if (ev.touches.length == 1) {
            // 获取当前触摸点的位置
            const currentX = ev.touches[0].clientX;
            const currentY = ev.touches[0].clientY;
            transform.value.offsetX = currentX - initialX
            transform.value.offsetY = currentY - initialY
        }
    }))
    const removeTouchend = useEventListener(document, 'touchend', () => {
        removeMousemove()
        removeTouchend()
    })
}

defineExpose({
    /**
     * @description manually switch image
     */
    setActiveItem,
})
</script>


<style>
.el-image-viewer__mask {
    height: calc(100% + 20px) !important;
}

.mobile .el-image-viewer__close {
    font-size: 19px;
    height: 33px;
    width: 33px;
    right: 20px;
    top: 20px;
}

.mobile .el-image-viewer__actions {
    height: 35px;
    width: 180px;
    padding: 0 15px;
    bottom: 20px;
}

.mobile .el-image-viewer__prev {
    left: 15px;
    width: 35px;
    height: 35px;
}

.mobile .el-image-viewer__next {
    right: 15px;
    width: 35px;
    height: 35px
}
</style>